package config;

import annotation.LazyAutowire;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import processor.AnnotationProcessor;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author jiang
 */
@Configuration
public class AnnotationsPostProcessor implements BeanPostProcessor, ApplicationContextAware, ApplicationRunner {

    private ApplicationContext applicationContext;

    private Set<Object> annotationBeans = new HashSet<>();

    @Bean("injectBeans")
    public Map<String, Object> injectBeans() {
        return new ConcurrentHashMap<>();
    }

    @Resource(name = "injectBeans")
    private Map<String, Object> injectBeans;


    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        Class<?> targetCls = bean.getClass();
        Set<AnnotationProcessor> annotationProcessors = new HashSet<>();
        Map<String, AnnotationProcessor> res = applicationContext.getBeansOfType(AnnotationProcessor.class);
        annotationBeans.addAll(res.values());
        Method[] declaredMethods = targetCls.getDeclaredMethods();
        for (AnnotationProcessor value : res.values()) {
            Class annotationClass = value.targetAnnotation();
            Annotation clazzAnnotation = AnnotationUtils.findAnnotation(targetCls, annotationClass);
            if (clazzAnnotation != null) {
                value.classInit(value.targetAnnotation(), bean);
            }
            Field[] allFields = FieldUtils.getAllFields(targetCls);
            for (Field allField : allFields) {
                Annotation fieldAnnotation = AnnotationUtils.findAnnotation(allField, annotationClass);
                if (fieldAnnotation != null) {
                    try {
                        Map<String, Object> map = new HashMap<>();
                        map.put("Bean", bean);
                        map.put("Field", allField);
                        map.put("AnnotationProcessor", value);
                        injectBeans.put(bean.getClass().getName() + allField.getName(), map);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        for (AnnotationProcessor value : res.values()) {
            Class annotationClass = value.targetAnnotation();
            for (Method declaredMethod : declaredMethods) {
                Annotation methodAnnotation = AnnotationUtils.findAnnotation(declaredMethod, annotationClass);
                if (methodAnnotation != null) {
                    annotationProcessors.add(value);
                }
                Parameter[] parameters = declaredMethod.getParameters();
                for (Parameter parameter : parameters) {
                    Annotation parameterAnnotation = AnnotationUtils.findAnnotation(parameter, annotationClass);
                    if (parameterAnnotation != null) {
                        annotationProcessors.add(value);
                    }
                }
            }
        }
        if (annotationProcessors.size() > 0) {
            CglibProxy proxy = new CglibProxy(annotationProcessors);
            Object o = proxy.newInstall(bean);
            Object cast = targetCls.cast(o);
            return cast;
        }
        return bean;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        for (Object annotationBean : annotationBeans) {
            Class<?> aClass = annotationBean.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
            Method[] declaredMethods = aClass.getDeclaredMethods();
            for (Field declaredField : declaredFields) {
                LazyAutowire annotation = AnnotationUtils.findAnnotation(declaredField, LazyAutowire.class);
                Object bean = null;
                if (annotation != null) {
                    String beanName = annotation.beanName();
                    if (!"".equals(beanName)) {
                        bean = applicationContext.getBean(beanName);
                    } else {
                        Class<?> type = declaredField.getType();
                        Map<String, ?> beansOfType = applicationContext.getBeansOfType(type);
                        List<?> objects = new ArrayList<>(beansOfType.values());
                        bean = objects.get(0);
                    }
                    try {
                        FieldUtils.writeDeclaredField(annotationBean, declaredField.getName(), bean, true);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            for (Method declaredMethod : declaredMethods) {
                LazyAutowire annotation = AnnotationUtils.findAnnotation(declaredMethod, LazyAutowire.class);
                Object bean = null;
                if (annotation != null) {
                    String beanName = annotation.beanName();
                    if (!"".equals(beanName)) {
                        bean = applicationContext.getBean(beanName);
                    } else {
                        Parameter[] parameters = declaredMethod.getParameters();
                        if (parameters.length == 1) {
                            Parameter parameter = parameters[0];
                            Class<?> type = parameter.getType();
                            Map<String, ?> beansOfType = applicationContext.getBeansOfType(type);
                            List<?> objects = new ArrayList<>(beansOfType.values());
                            bean = objects.get(0);
                        }
                    }
                    try {
                        MethodUtils.invokeMethod(annotationBean, declaredMethod.getName(), bean);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
            for (Object injectBean : injectBeans.values()) {
                Map<String, Object> injectBeanMap = (Map<String, Object>) injectBean;
                Object bean = injectBeanMap.get("Bean");
                Field allField = (Field) injectBeanMap.get("Field");
                AnnotationProcessor annotation = (AnnotationProcessor) injectBeanMap.get("AnnotationProcessor");
                Class<?> targetClass = bean.getClass();
                Map beansOfType = applicationContext.getBeansOfType(targetClass);
                Collection values = beansOfType.values();
                for (Object value : values) {
                    if (targetClass != value.getClass()) {
                        Field[] fields = value.getClass().getSuperclass().getDeclaredFields();
                        for (Field field : fields) {
                            field.setAccessible(true);
                            Object o = field.get(bean);
                            if (o != null) {
                                field.set(value, o);
                            }
                        }
                    }
                    allField.setAccessible(true);
                    Object o = allField.get(bean);
                    annotation.fieldInit(annotation.targetAnnotation(), value, allField, o);
                }
            }
        }
    }
}

